/**
 * Base.java
 *
 * Roslan's Remake of MOO in 3D using Ardor3D
 * (C) 2011 Roslan Amir. All rights reserved.
 */

package com.roslan.games.moo3d;

import java.net.URISyntaxException;
import java.util.prefs.Preferences;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ardor3d.annotation.MainThread;
import com.ardor3d.framework.Canvas;
import com.ardor3d.framework.CanvasRenderer;
import com.ardor3d.framework.DisplaySettings;
import com.ardor3d.framework.FrameHandler;
import com.ardor3d.framework.NativeCanvas;
import com.ardor3d.framework.Scene;
import com.ardor3d.framework.Updater;
import com.ardor3d.framework.jogl.JoglCanvas;
import com.ardor3d.framework.jogl.JoglCanvasRenderer;
import com.ardor3d.framework.lwjgl.LwjglCanvas;
import com.ardor3d.framework.lwjgl.LwjglCanvasRenderer;
import com.ardor3d.image.util.AWTImageLoader;
import com.ardor3d.image.util.ScreenShotImageExporter;
import com.ardor3d.input.Key;
import com.ardor3d.input.MouseButton;
import com.ardor3d.input.MouseManager;
import com.ardor3d.input.PhysicalLayer;
import com.ardor3d.input.awt.AwtFocusWrapper;
import com.ardor3d.input.awt.AwtKeyboardWrapper;
import com.ardor3d.input.awt.AwtMouseManager;
import com.ardor3d.input.awt.AwtMouseWrapper;
import com.ardor3d.input.logical.AnyKeyCondition;
import com.ardor3d.input.logical.DummyControllerWrapper;
import com.ardor3d.input.logical.InputTrigger;
import com.ardor3d.input.logical.KeyPressedCondition;
import com.ardor3d.input.logical.LogicalLayer;
import com.ardor3d.input.logical.MouseButtonClickedCondition;
import com.ardor3d.input.logical.MouseButtonPressedCondition;
import com.ardor3d.input.logical.TriggerAction;
import com.ardor3d.input.logical.TwoInputStates;
import com.ardor3d.input.lwjgl.LwjglControllerWrapper;
import com.ardor3d.input.lwjgl.LwjglKeyboardWrapper;
import com.ardor3d.input.lwjgl.LwjglMouseManager;
import com.ardor3d.input.lwjgl.LwjglMouseWrapper;
import com.ardor3d.intersection.PickData;
import com.ardor3d.intersection.PickResults;
import com.ardor3d.intersection.Pickable;
import com.ardor3d.intersection.PickingUtil;
import com.ardor3d.intersection.PrimitivePickResults;
import com.ardor3d.light.PointLight;
import com.ardor3d.math.ColorRGBA;
import com.ardor3d.math.Ray3;
import com.ardor3d.math.Vector2;
import com.ardor3d.math.Vector3;
import com.ardor3d.renderer.ContextCapabilities;
import com.ardor3d.renderer.ContextManager;
import com.ardor3d.renderer.Renderer;
import com.ardor3d.renderer.TextureRendererFactory;
import com.ardor3d.renderer.jogl.JoglTextureRendererProvider;
import com.ardor3d.renderer.lwjgl.LwjglTextureRendererProvider;
import com.ardor3d.renderer.queue.RenderBucketType;
import com.ardor3d.renderer.state.LightState;
import com.ardor3d.renderer.state.WireframeState;
import com.ardor3d.renderer.state.ZBufferState;
import com.ardor3d.scenegraph.Node;
import com.ardor3d.scenegraph.event.DirtyType;
import com.ardor3d.util.Constants;
import com.ardor3d.util.ContextGarbageCollector;
import com.ardor3d.util.GameTaskQueue;
import com.ardor3d.util.GameTaskQueueManager;
import com.ardor3d.util.ReadOnlyTimer;
import com.ardor3d.util.Timer;
import com.ardor3d.util.resource.ResourceLocatorTool;
import com.ardor3d.util.resource.SimpleResourceLocator;
import com.ardor3d.util.screen.ScreenExporter;
import com.ardor3d.util.stat.StatCollector;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

/**
 * Comments go here.
 * 
 * @author Roslan Amir
 * @version 1.0 - Jan 17, 2011
 */
public abstract class Base implements Runnable, Updater, Scene {

	/**
	 * Display settings
	 */
	private static final String DEFAULT_SCREEN_RENDERER = "LWJGL"; // LWJGL or JOGL

	private static final int DEFAULT_SCREEN_WIDTH = 1024;

	private static final int DEFAULT_SCREEN_HEIGHT = 768;

	private static final int DEFAULT_SCREEN_COLOR_DEPTH = 24;

	private static final int DEFAULT_SCREEN_FREQUENCY = 60;

	private static final boolean DEFAULT_SCREEN_FULLSCREEN = false;

	/**
	 * 
	 */
	protected static final Logger logger = LoggerFactory.getLogger(Base.class);

    /**
     * If true (the default) we will call System.exit on end of demo.
     */
    public static boolean QUIT_VM_ON_EXIT = true;

    /**
     * 
     */
	protected NativeCanvas _canvas;

	/**
	 * 
	 */
	protected PhysicalLayer _physicalLayer;

	/**
     * 
     */
	protected MouseManager _mouseManager;

	/**
     * 
     */
	protected final LogicalLayer _logicalLayer = new LogicalLayer();

	/**
     * 
     */
	protected final Timer _timer = new Timer();

	/**
     * 
     */
	protected final FrameHandler _frameHandler = new FrameHandler(_timer);

	/**
     * 
     */
	protected final Node _root = new Node();

	/**
	 * The flag to indicate the end of the game is requested.
	 */
	protected volatile boolean _exit = false;

	/**
     * 
     */
	protected LightState _lightState;

	/**
     * 
     */
	protected WireframeState _wireframeState;

	/**
     * 
     */
	protected boolean _doShot = false;

    /**
     * 
     */
    protected ScreenShotImageExporter _screenShotExp = new ScreenShotImageExporter();

	/**
	 * 
	 */
	private static Preferences prefs = Preferences.userNodeForPackage(Base.class);

	/**
	 * Comment for method.
	 * 
	 * @param cls - the subclass of Base to start
	 */
	public static void start(final Class<? extends Base> cls) {
		Base instance = null;
		try {
			instance = cls.newInstance();
		} catch (Exception ex) {
			logger.error("Exception when creating the class instance", ex);
			System.exit(255);
		}

		/*
		 * Retrieve the graphics parameters from user preference.
		 */
		GameData.screenWidth = prefs.getInt("/screen/width", DEFAULT_SCREEN_WIDTH);
		GameData.screenHeight = prefs.getInt("/screen/height", DEFAULT_SCREEN_HEIGHT);
		GameData.screenColorDepth = prefs.getInt("/screen/colordepth", DEFAULT_SCREEN_COLOR_DEPTH);
		GameData.screenFrequency = prefs.getInt("/screen/frequency", DEFAULT_SCREEN_FREQUENCY);
		GameData.screenFullScreen = prefs.getBoolean("/screen/fullscreen", DEFAULT_SCREEN_FULLSCREEN);

		/*
		 * Set up the display settings.
		 */
		DisplaySettings settings = new DisplaySettings(GameData.screenWidth, GameData.screenHeight, GameData.screenColorDepth, GameData.screenFrequency, GameData.screenFullScreen);

		/*
		 * Get our preferred framework.
		 */
		if (DEFAULT_SCREEN_RENDERER.equalsIgnoreCase("LWJGL")) {
			final LwjglCanvasRenderer canvasRenderer = new LwjglCanvasRenderer(instance);
			instance._canvas = new LwjglCanvas(canvasRenderer, settings);
			instance._physicalLayer = new PhysicalLayer(new LwjglKeyboardWrapper(), new LwjglMouseWrapper(), new LwjglControllerWrapper(), (LwjglCanvas) instance._canvas);
			instance._mouseManager = new LwjglMouseManager();
			TextureRendererFactory.INSTANCE.setProvider(new LwjglTextureRendererProvider());
		} else if (DEFAULT_SCREEN_RENDERER.equalsIgnoreCase("JOGL")) {
			final JoglCanvasRenderer canvasRenderer = new JoglCanvasRenderer(instance);
			instance._canvas = new JoglCanvas(canvasRenderer, settings);
			final JoglCanvas canvas = (JoglCanvas) instance._canvas;
			instance._mouseManager = new AwtMouseManager(canvas);
			instance._physicalLayer = new PhysicalLayer(new AwtKeyboardWrapper(canvas), new AwtMouseWrapper(canvas, instance._mouseManager), DummyControllerWrapper.INSTANCE, new AwtFocusWrapper(canvas));
			TextureRendererFactory.INSTANCE.setProvider(new JoglTextureRendererProvider());
		} else {
			logger.warn("No renderer specified -- use either LWJGL or JOGL");
			return;
		}

		instance._logicalLayer.registerInput(instance._canvas, instance._physicalLayer);

		/*
		 * Register our instance as an updater.
		 */
		instance._frameHandler.addUpdater(instance);

		/*
		 * register our native canvas.
		 */
		instance._frameHandler.addCanvas(instance._canvas);

		/*
		 * Start the thread.
		 */
		new Thread(instance).start();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.ardor3d.framework.Updater#init()
	 */
	@MainThread
	public void init() {
		final ContextCapabilities caps = ContextManager.getCurrentContext().getCapabilities();
		logger.info("Display Vendor: " + caps.getDisplayVendor());
		logger.info("Display Renderer: " + caps.getDisplayRenderer());
		logger.info("Display Version: " + caps.getDisplayVersion());
		logger.info("Shading Language Version: " + caps.getShadingLanguageVersion());

		AWTImageLoader.registerLoader();
		try {
			SimpleResourceLocator srl = new SimpleResourceLocator(ResourceLocatorTool.getClassPathResource(Base.class, "com/ardor3d/example/media/"));
			ResourceLocatorTool.addResourceLocator(ResourceLocatorTool.TYPE_TEXTURE, srl);
			srl = new SimpleResourceLocator(ResourceLocatorTool.getClassPathResource(Base.class, "com/ardor3d/example/media/models/"));
			ResourceLocatorTool.addResourceLocator(ResourceLocatorTool.TYPE_MODEL, srl);
		} catch (final URISyntaxException ex) {
			logger.error("Exception when registering keyboard and mouse triggers", ex);
			System.exit(255);
		}

		/*
		 * Create a ZBuffer to display pixels closest to the camera above farther ones.
		 */
		final ZBufferState buf = new ZBufferState();
		buf.setEnabled(true);
		buf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);
		_root.setRenderState(buf);

		// ---- LIGHTS ----

		/*
		 * Set up a basic, default light.
		 */
		final PointLight light = new PointLight();
		light.setDiffuse(new ColorRGBA(0.75f, 0.75f, 0.75f, 0.75f));
		light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
		light.setLocation(new Vector3(100, 100, 100));
		light.setEnabled(true);

		/*
		 * Attach the light to a LightState and the LightState to the root node.
		 */
		_lightState = new LightState();
		_lightState.setEnabled(true);
		_lightState.attach(light);
		_root.setRenderState(_lightState);

		_wireframeState = new WireframeState();
		_wireframeState.setEnabled(false);
		_root.setRenderState(_wireframeState);

		_root.getSceneHints().setRenderBucketType(RenderBucketType.Opaque);

		/*
		 * Register the keyboard and mouse triggers.
		 */
		registerInputTriggers();

		/*
		 * Call the subclass's initialization method.
		 */
		initGame();

		_root.updateGeometricState(0);
	}

	/**
	 * Comment for method.
	 */
	protected void registerInputTriggers() {
		/*
		 * Check if this example worries about input at all.
		 */
		if (_logicalLayer == null) {
			return;
		}

		_logicalLayer.registerTrigger(new InputTrigger(new MouseButtonClickedCondition(MouseButton.LEFT), new TriggerAction() {
			public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
				final Vector2 pos = Vector2.fetchTempInstance().set(inputStates.getCurrent().getMouseState().getX(), inputStates.getCurrent().getMouseState().getY());
				final Ray3 pickRay = new Ray3();
				_canvas.getCanvasRenderer().getCamera().getPickRay(pos, false, pickRay);
				Vector2.releaseTempInstance(pos);
				doPick(pickRay);
			}
		}, "pickTrigger"));

		_logicalLayer.registerTrigger(new InputTrigger(new KeyPressedCondition(Key.ESCAPE), new TriggerAction() {
			public void perform(final Canvas source, final TwoInputStates inputState, final double tpf) {
				exit();
			}
		}));

		_logicalLayer.registerTrigger(new InputTrigger(new KeyPressedCondition(Key.T), new TriggerAction() {
			public void perform(final Canvas source, final TwoInputStates inputState, final double tpf) {
				_wireframeState.setEnabled(!_wireframeState.isEnabled());
				// Either an update or a markDirty is needed here since we did not touch the affected spatial directly.
				_root.markDirty(DirtyType.RenderState);
			}
		}));

		_logicalLayer.registerTrigger(new InputTrigger(new KeyPressedCondition(Key.F1), new TriggerAction() {
			public void perform(final Canvas source, final TwoInputStates inputState, final double tpf) {
				_doShot = true;
			}
		}));

		final Predicate<TwoInputStates> someMousePressed = Predicates.or(new MouseButtonPressedCondition(MouseButton.RIGHT), Predicates.or(new MouseButtonPressedCondition(MouseButton.MIDDLE), new MouseButtonPressedCondition(MouseButton.LEFT)));

		_logicalLayer.registerTrigger(new InputTrigger(someMousePressed, new TriggerAction() {
			public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
				logger.warn("clicked: " + inputStates.getCurrent().getMouseState().getClickCounts());
			}
		}));

//		final Predicate<TwoInputStates> clickLeftOrRight = Predicates.or(new MouseButtonClickedCondition(MouseButton.LEFT), new MouseButtonClickedCondition(MouseButton.RIGHT));

//		_logicalLayer.registerTrigger(new InputTrigger(clickLeftOrRight, new TriggerAction() {
//			public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
//				logger.warn("clicked: " + inputStates.getCurrent().getMouseState().getClickCounts());
//			}
//		}));

//		_logicalLayer.registerTrigger(new InputTrigger(new MouseButtonPressedCondition(MouseButton.LEFT), new TriggerAction() {
//			public void perform(final Canvas source, final TwoInputStates inputState, final double tpf) {
//				if (_mouseManager.isSetGrabbedSupported()) {
//					_mouseManager.setGrabbed(GrabbedState.GRABBED);
//				}
//			}
//		}));
//		_logicalLayer.registerTrigger(new InputTrigger(new MouseButtonReleasedCondition(MouseButton.LEFT), new TriggerAction() {
//			public void perform(final Canvas source, final TwoInputStates inputState, final double tpf) {
//				if (_mouseManager.isSetGrabbedSupported()) {
//					_mouseManager.setGrabbed(GrabbedState.NOT_GRABBED);
//				}
//			}
//		}));

		_logicalLayer.registerTrigger(new InputTrigger(new AnyKeyCondition(), new TriggerAction() {
			public void perform(final Canvas source, final TwoInputStates inputState, final double tpf) {
				logger.info("Key character pressed: " + inputState.getCurrent().getKeyboardState().getKeyEvent().getKeyChar());
			}
		}));

	}

	/**
	 * Set the exit flag to true.
	 */
	protected void exit() {
		_exit = true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		try {
			_frameHandler.init();

			while (!_exit) {
				_frameHandler.updateFrame();
				Thread.yield();
			}

			/*
			 * Grab the graphics context so cleanup will work out.
			 */
			final CanvasRenderer cr = _canvas.getCanvasRenderer();
			cr.makeCurrentContext();
			quit(cr.getRenderer());
			cr.releaseCurrentContext();
			if (QUIT_VM_ON_EXIT) {
				System.exit(0);
			}
		} catch (final Throwable t) {
			logger.error("Throwable caught in Main Thread - exiting", t);
		}
	}

    /**
     * Comment for method.
     *
     * @param renderer
     */
    protected void quit(final Renderer renderer) {
        ContextGarbageCollector.doFinalCleanup(renderer);
        _canvas.close();
    }

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.ardor3d.framework.Updater#update(com.ardor3d.util.ReadOnlyTimer)
	 */
    @MainThread
	public void update(final ReadOnlyTimer timer) {
		if (_canvas.isClosing()) {
			exit();
		}

		/*
		 * Update stats, if enabled.
		 */
		if (Constants.stats) {
			StatCollector.update();
		}

		updateLogicalLayer(timer);

		/*
		 * Execute updateQueue item.
		 */
		GameTaskQueueManager.getManager(_canvas.getCanvasRenderer().getRenderContext()).getQueue(GameTaskQueue.UPDATE).execute();

		/*
		 * Call updateGame in any derived classes of Base.
		 */
		updateGame(timer);

		/*
		 * Update controllers/render states/transforms/bounds for root Node.
		 */
		_root.updateGeometricState(timer.getTimePerFrame(), true);
	}

	/**
	 * Check and execute any input triggers, if we are concerned with input Comment for method.
	 * 
	 * @param timer
	 */
	protected void updateLogicalLayer(final ReadOnlyTimer timer) {
		if (_logicalLayer != null) {
			_logicalLayer.checkTriggers(timer.getTimePerFrame());
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.ardor3d.framework.Scene#renderUnto(com.ardor3d.renderer.Renderer)
	 */
	@MainThread
	public boolean renderUnto(Renderer renderer) {
		/*
		 * Execute renderQueue item.
		 */
		GameTaskQueueManager.getManager(_canvas.getCanvasRenderer().getRenderContext()).getQueue(GameTaskQueue.RENDER).execute(renderer);

		/*
		 * Clean up card garbage such as textures, vbos, etc.
		 */
		ContextGarbageCollector.doRuntimeCleanup(renderer);

		/*
		 * Draw the root Node and all its children.
		 */
		if (!_canvas.isClosing()) {
			/*
			 * Call renderGame in any derived classes.
			 */
			renderGame(renderer);
//			renderDebug(renderer);

			if (_doShot) {
				/*
				 * Force any waiting scene elements to be renderer.
				 */
				renderer.renderBuckets();
				ScreenExporter.exportCurrentScreen(_canvas.getCanvasRenderer().getRenderer(), _screenShotExp);
				_doShot = false;
			}
			return true;
		}
		return false;
	}

    /**
     * Comment for method.
     *
     * @param renderer
     */
    protected void renderGame(final Renderer renderer) {
        _root.onDraw(renderer);
    }

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.ardor3d.framework.Scene#doPick(com.ardor3d.math.Ray3)
	 */
	public PickResults doPick(final Ray3 pickRay) {
		final PrimitivePickResults pickResults = new PrimitivePickResults();
		pickResults.setCheckDistance(true);
		PickingUtil.findPick(_root, pickRay, pickResults);

		int i = 0;
		while (pickResults.getNumber() > 0 && pickResults.getPickData(i).getIntersectionRecord().getNumberOfIntersections() == 0 && ++i < pickResults.getNumber()) {
		}
		if (pickResults.getNumber() > i) {
			final PickData pick = pickResults.getPickData(i);
			Pickable target = pick.getTarget();
			logger.info("Picked " + target + " at " + pick.getIntersectionRecord().getIntersectionPoint(0));
			processPick(pick, target);
		} else {
			logger.info("Picked nothing!");
		}

		return pickResults;
	}

	/**
	 * Comment for method.
	 */
	protected abstract void initGame();

	/**
	 * Comment for method.
	 *
	 * @param timer
	 */
	protected abstract void updateGame(ReadOnlyTimer timer);

	/**
	 * Process the target of the pick.
	 *
	 * @param pick 
	 * @param target - the target of the pick
	 */
	protected abstract void processPick(PickData pick, Pickable target);

}
